// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io' as io;

import 'package:_fe_analyzer_shared/src/base/syntactic_entity.dart';
import 'package:analysis_server/src/protocol_server.dart' show ElementKind;
import 'package:analysis_server/src/services/completion/dart/feature_computer.dart';
import 'package:analysis_server/src/utilities/extensions/ast.dart';
import 'package:analyzer/dart/analysis/analysis_context_collection.dart';
import 'package:analyzer/dart/analysis/context_root.dart';
import 'package:analyzer/dart/analysis/results.dart';
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/dart/ast/visitor.dart';
import 'package:analyzer/dart/element/element.dart'
    show Element, InterfaceElement, LibraryElement;
import 'package:analyzer/dart/element/type_provider.dart';
import 'package:analyzer/dart/element/type_system.dart';
import 'package:analyzer/file_system/file_system.dart';
import 'package:analyzer/file_system/physical_file_system.dart';
import 'package:analyzer/src/util/file_paths.dart' as file_paths;
import 'package:analyzer/src/utilities/extensions/diagnostic.dart';
import 'package:analyzer/src/utilities/extensions/flutter.dart';
import 'package:analyzer_testing/package_root.dart' as package_root;
import 'package:analyzer_utilities/tools.dart';
import 'package:args/args.dart';

/// Compute metrics to determine whether they should be used to compute a
/// relevance score for completion suggestions.
Future<void> main(List<String> args) async {
  var parser = createArgParser();
  var result = parser.parse(args);

  if (validArguments(parser, result)) {
    var provider = PhysicalResourceProvider.INSTANCE;
    var packageRoot = provider.pathContext.normalize(package_root.packageRoot);

    void writeRelevanceTable(RelevanceData data, {String suffix = ''}) {
      var generatedFilePath = provider.pathContext.join(
        packageRoot,
        'analysis_server',
        'lib',
        'src',
        'services',
        'completion',
        'dart',
        'relevance_tables$suffix.g.dart',
      );
      var generatedFile = provider.getFile(generatedFilePath);

      var buffer = StringBuffer();
      var writer = RelevanceTableWriter(buffer);
      writer.write(data);
      generatedFile.writeAsStringSync(buffer.toString());
      DartFormat.formatFile(io.File(generatedFile.path));
    }

    if (result.wasParsed('reduceDir')) {
      var data = RelevanceData();
      var dir = provider.getFolder(result['reduceDir'] as String);
      var suffix = result.rest.isNotEmpty ? result.rest[0] : '';
      for (var child in dir.getChildren()) {
        if (child is File) {
          var newData = RelevanceData.fromJson(child.readAsStringSync());
          data.addData(newData);
        }
      }
      writeRelevanceTable(data, suffix: suffix);
      return;
    }

    var rootPath = result.rest[0];
    print('Analyzing root: "$rootPath"');

    var computer = RelevanceMetricsComputer();
    var stopwatch = Stopwatch()..start();
    await computer.compute(rootPath, verbose: result['verbose'] as bool);
    if (result.wasParsed('mapFile')) {
      var mapFile = provider.getFile(result['mapFile'] as String);
      mapFile.writeAsStringSync(computer.data.toJson());
    } else {
      writeRelevanceTable(computer.data);
    }
    stopwatch.stop();

    var duration = Duration(milliseconds: stopwatch.elapsedMilliseconds);
    print('Tables generated in $duration');
  }
}

/// Create a parser that can be used to parse the command-line arguments.
ArgParser createArgParser() {
  var parser = ArgParser();
  parser.addFlag(
    'help',
    abbr: 'h',
    help: 'Print this help message.',
    negatable: false,
  );
  parser.addOption(
    'mapFile',
    help:
        'The absolute path of the file to which the relevance data will be '
        'written. Using this option will prevent the relevance table from '
        'being written.',
  );
  parser.addOption(
    'reduceDir',
    help:
        'The absolute path of the directory from which the relevance data '
        'will be read.',
  );
  parser.addFlag(
    'verbose',
    abbr: 'v',
    help: 'Print additional information about the analysis',
    negatable: false,
  );
  return parser;
}

/// Print usage information for this tool.
void printUsage(ArgParser parser, {String? error}) {
  if (error != null) {
    print(error);
    print('');
  }
  print('usage: dart relevance_table_generator.dart [options] packagePath');
  print('');
  print('Generate the tables used to compute the values of certain features.');
  print('');
  print(parser.usage);
}

/// Return `true` if the command-line arguments (represented by the [result] and
/// parsed by the [parser]) are valid.
bool validArguments(ArgParser parser, ArgResults result) {
  if (result.wasParsed('help')) {
    printUsage(parser);
    return false;
  } else if (result.wasParsed('reduceDir')) {
    return validateDir(parser, result['reduceDir'] as String);
  } else if (result.rest.length != 1) {
    printUsage(parser, error: 'No package path specified.');
    return false;
  }
  if (result.wasParsed('mapFile')) {
    var mapFilePath = result['mapFile'];
    if (mapFilePath is! String ||
        !PhysicalResourceProvider.INSTANCE.pathContext.isAbsolute(
          mapFilePath,
        )) {
      printUsage(
        parser,
        error: 'The path "$mapFilePath" must be an absolute path.',
      );
      return false;
    }
  }
  return validateDir(parser, result.rest[0]);
}

/// Return `true` if the [dirPath] is an absolute path to a directory that
/// exists.
bool validateDir(ArgParser parser, String dirPath) {
  if (!PhysicalResourceProvider.INSTANCE.pathContext.isAbsolute(dirPath)) {
    printUsage(parser, error: 'The path "$dirPath" must be an absolute path.');
    return false;
  }
  if (!io.Directory(dirPath).existsSync()) {
    printUsage(parser, error: 'The directory "$dirPath" does not exist.');
    return false;
  }
  return true;
}

/// An object that records the data used to compute the tables.
class RelevanceData {
  /// A table mapping element kinds and keywords to counts by context.
  final Map<String, Map<_Kind, int>> _byKind = {};

  /// Initialize a newly created set of relevance data to be empty.
  RelevanceData();

  /// Initialize a newly created set of relevance data based on the content of
  /// the JSON encoded string.
  RelevanceData.fromJson(String encoded) {
    var map = json.decode(encoded) as Map<String, dynamic>;
    for (var contextEntry in map.entries) {
      var contextMap = _byKind.putIfAbsent(contextEntry.key, () => {});
      for (var kindEntry
          in (contextEntry.value as Map<String, dynamic>).entries) {
        _Kind kind;
        var key = kindEntry.key;
        if (key.startsWith('e')) {
          kind = _ElementKind(ElementKind.values.byName(key.substring(1)));
        } else if (key.startsWith('k')) {
          kind = _Keyword(Keyword.keywords[key.substring(1)]!);
        } else {
          throw StateError('Invalid initial character in unique key "$key"');
        }
        contextMap[kind] = int.parse(kindEntry.value as String);
      }
    }
  }

  /// Add the data from the given relevance [data] to this set of relevance
  /// data.
  void addData(RelevanceData data) {
    for (var contextEntry in data._byKind.entries) {
      var contextMap = _byKind.putIfAbsent(contextEntry.key, () => {});
      for (var kindEntry in contextEntry.value.entries) {
        var kind = kindEntry.key;
        contextMap[kind] = (contextMap[kind] ?? 0) + kindEntry.value;
      }
    }
  }

  /// Record that an element of the given [kind] was found in the given
  /// [context].
  void recordElementKind(String context, ElementKind kind) {
    var contextMap = _byKind.putIfAbsent(context, () => {});
    var key = _ElementKind(kind);
    contextMap[key] = (contextMap[key] ?? 0) + 1;
  }

  /// Record that the given [keyword] was found in the given [context].
  void recordKeyword(String context, Keyword keyword) {
    var contextMap = _byKind.putIfAbsent(context, () => {});
    var key = _Keyword(keyword);
    contextMap[key] = (contextMap[key] ?? 0) + 1;
  }

  /// Convert this data to a JSON encoded format.
  String toJson() {
    var map = <String, Map<String, String>>{};
    for (var contextEntry in _byKind.entries) {
      var kindMap = <String, String>{};
      for (var kindEntry in contextEntry.value.entries) {
        kindMap[kindEntry.key.uniqueKey] = kindEntry.value.toString();
      }
      map[contextEntry.key] = kindMap;
    }
    return json.encode(map);
  }
}

/// An object that visits a compilation unit in order to record the data used to
/// compute the metrics.
class RelevanceDataCollector extends RecursiveAstVisitor<void> {
  static const List<Keyword> declarationKeywords = [
    Keyword.MIXIN,
    Keyword.TYPEDEF,
  ];

  static const List<Keyword> directiveKeywords = [
    Keyword.EXPORT,
    Keyword.IMPORT,
    Keyword.LIBRARY,
    Keyword.PART,
  ];

  static const List<Keyword> exportKeywords = [
    Keyword.AS,
    Keyword.HIDE,
    Keyword.SHOW,
  ];

  static const List<Keyword> expressionKeywords = [
    Keyword.AWAIT,
    Keyword.SUPER,
  ];

  static const List<Keyword> functionBodyKeywords = [
    Keyword.ASYNC,
    Keyword.SYNC,
  ];

  static const List<Keyword> importKeywords = [
    Keyword.AS,
    Keyword.HIDE,
    Keyword.SHOW,
  ];

  static const List<Keyword> memberKeywords = [
    Keyword.FACTORY,
    Keyword.GET,
    Keyword.OPERATOR,
    Keyword.SET,
    Keyword.STATIC,
  ];

  static const List<Keyword> noKeywords = [];

  static const List<Keyword> statementKeywords = [Keyword.AWAIT, Keyword.YIELD];

  /// The relevance data being collected.
  final RelevanceData data;

  /// The compilation unit in which data is currently being collected.
  late CompilationUnit unit;

  /// The library containing the compilation unit being visited.
  late LibraryElement enclosingLibrary;

  /// The type provider associated with the current compilation unit.
  late TypeProvider typeProvider;

  /// The type system associated with the current compilation unit.
  late TypeSystem typeSystem;

  /// The object used to compute the values of features.
  late FeatureComputer featureComputer;

  /// Initialize a newly created collector to add data points to the given
  /// [data].
  RelevanceDataCollector(this.data);

  /// Initialize this collector prior to visiting the unit in the [result].
  void initializeFrom(ResolvedUnitResult result) {
    unit = result.unit;
  }

  @override
  void visitAdjacentStrings(AdjacentStrings node) {
    // There are no completions.
    super.visitAdjacentStrings(node);
  }

  @override
  void visitAnnotation(Annotation node) {
    _recordDataForNode('Annotation_name', node.name);
    super.visitAnnotation(node);
  }

  @override
  void visitArgumentList(ArgumentList node) {
    var context = _argumentListContext(node);
    for (var argument in node.arguments) {
      var realArgument = argument;
      var argumentKind = 'unnamed';
      if (argument is NamedExpression) {
        realArgument = argument.expression;
        argumentKind = 'named';
      }
      _recordDataForNode(
        'ArgumentList_${context}_$argumentKind',
        realArgument,
        allowedKeywords: expressionKeywords,
      );
    }
    super.visitArgumentList(node);
  }

  @override
  void visitAsExpression(AsExpression node) {
    _recordDataForNode('AsExpression_type', node.type);
    super.visitAsExpression(node);
  }

  @override
  void visitAssertInitializer(AssertInitializer node) {
    _recordDataForNode(
      'AssertInitializer_condition',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode(
      'AssertInitializer_message',
      node.message,
      allowedKeywords: expressionKeywords,
    );
    super.visitAssertInitializer(node);
  }

  @override
  void visitAssertStatement(AssertStatement node) {
    _recordDataForNode(
      'AssertStatement_condition',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode(
      'AssertStatement_message',
      node.message,
      allowedKeywords: expressionKeywords,
    );
    super.visitAssertStatement(node);
  }

  @override
  void visitAssignmentExpression(AssignmentExpression node) {
    _recordDataForNode(
      'AssignmentExpression_rightHandSide',
      node.rightHandSide,
      allowedKeywords: expressionKeywords,
    );
    super.visitAssignmentExpression(node);
  }

  @override
  void visitAwaitExpression(AwaitExpression node) {
    _recordDataForNode(
      'AwaitExpression_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitAwaitExpression(node);
  }

  @override
  void visitBinaryExpression(BinaryExpression node) {
    _recordDataForNode(
      'BinaryExpression_${node.operator}_rightOperand',
      node.rightOperand,
      allowedKeywords: expressionKeywords,
    );
    super.visitBinaryExpression(node);
  }

  @override
  void visitBlock(Block node) {
    for (var statement in node.statements) {
      // Function declaration statements that have no return type begin with an
      // identifier but don't have an element kind associated with the
      // identifier.
      _recordDataForNode(
        'Block_statement',
        statement,
        allowedKeywords: statementKeywords,
      );
    }
    super.visitBlock(node);
  }

  @override
  void visitBlockFunctionBody(BlockFunctionBody node) {
    _recordKeyword(
      'BlockFunctionBody_start',
      node,
      allowedKeywords: functionBodyKeywords,
    );
    super.visitBlockFunctionBody(node);
  }

  @override
  void visitBooleanLiteral(BooleanLiteral node) {
    _recordKeyword('BooleanLiteral_start', node);
    super.visitBooleanLiteral(node);
  }

  @override
  void visitBreakStatement(BreakStatement node) {
    // The token following the `break` (if there is one) is always a label.
    super.visitBreakStatement(node);
  }

  @override
  void visitCascadeExpression(CascadeExpression node) {
    for (var cascade in node.cascadeSections) {
      _recordDataForNode('CascadeExpression_cascadeSection', cascade);
    }
    super.visitCascadeExpression(node);
  }

  @override
  void visitCatchClause(CatchClause node) {
    _recordDataForNode('CatchClause_exceptionType', node.exceptionType);
    super.visitCatchClause(node);
  }

  @override
  void visitClassDeclaration(ClassDeclaration node) {
    var context = 'name';
    if (node.extendsClause != null) {
      _recordKeyword(
        'ClassDeclaration_$context',
        node.extendsClause,
        allowedKeywords: [Keyword.EXTENDS],
      );
      context = 'extends';
    }
    if (node.withClause != null) {
      _recordKeyword('ClassDeclaration_$context', node.withClause);
      context = 'with';
    }
    _recordKeyword(
      'ClassDeclaration_$context',
      node.implementsClause,
      allowedKeywords: [Keyword.IMPLEMENTS],
    );

    for (var member in node.members2) {
      _recordDataForNode(
        'ClassDeclaration_member',
        member,
        allowedKeywords: memberKeywords,
      );
    }
    super.visitClassDeclaration(node);
  }

  @override
  void visitClassTypeAlias(ClassTypeAlias node) {
    _recordDataForNode('ClassTypeAlias_superclass', node.superclass);
    var context = 'superclass';
    _recordKeyword('ClassTypeAlias_$context', node.withClause);
    context = 'with';
    _recordKeyword('ClassTypeAlias_$context', node.implementsClause);
    super.visitClassTypeAlias(node);
  }

  @override
  void visitComment(Comment node) {
    // There are no completions.
    super.visitComment(node);
  }

  @override
  void visitCommentReference(CommentReference node) {
    _recordDataForNode('CommentReference_expression', node.expression);
    super.visitCommentReference(node);
  }

  @override
  void visitCompilationUnit(CompilationUnit node) {
    enclosingLibrary = node.declaredFragment!.element;
    typeProvider = enclosingLibrary.typeProvider;
    typeSystem = enclosingLibrary.typeSystem;
    featureComputer = FeatureComputer(typeSystem, typeProvider);

    for (var directive in node.directives) {
      _recordKeyword(
        'CompilationUnit_directive',
        directive,
        allowedKeywords: directiveKeywords,
      );
    }
    for (var declaration in node.declarations) {
      _recordDataForNode(
        'CompilationUnit_declaration',
        declaration,
        allowedKeywords: declarationKeywords,
      );
    }
    super.visitCompilationUnit(node);
  }

  @override
  void visitConditionalExpression(ConditionalExpression node) {
    _recordDataForNode(
      'ConditionalExpression_thenExpression',
      node.thenExpression,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode(
      'ConditionalExpression_elseExpression',
      node.elseExpression,
      allowedKeywords: expressionKeywords,
    );
    super.visitConditionalExpression(node);
  }

  @override
  void visitConfiguration(Configuration node) {
    // There are no completions.
    super.visitConfiguration(node);
  }

  @override
  void visitConstructorDeclaration(ConstructorDeclaration node) {
    // TODO(scheglov): support primary constructors
    _recordDataForNode('ConstructorDeclaration_returnType', node.typeName!);
    for (var initializer in node.initializers) {
      _recordDataForNode('ConstructorDeclaration_initializer', initializer);
    }
    super.visitConstructorDeclaration(node);
  }

  @override
  void visitConstructorFieldInitializer(ConstructorFieldInitializer node) {
    _recordDataForNode(
      'ConstructorFieldInitializer_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitConstructorFieldInitializer(node);
  }

  @override
  void visitConstructorName(ConstructorName node) {
    // The token following the `.` is always the name of a constructor.
    super.visitConstructorName(node);
  }

  @override
  void visitContinueStatement(ContinueStatement node) {
    // The token following the `continue` (if there is one) is always a label.
    super.visitContinueStatement(node);
  }

  @override
  void visitDeclaredIdentifier(DeclaredIdentifier node) {
    // There are no completions.
    super.visitDeclaredIdentifier(node);
  }

  @override
  void visitDefaultFormalParameter(DefaultFormalParameter node) {
    _recordDataForNode(
      'DefaultFormalParameter_defaultValue',
      node.defaultValue,
      allowedKeywords: expressionKeywords,
    );
    super.visitDefaultFormalParameter(node);
  }

  @override
  void visitDoStatement(DoStatement node) {
    _recordDataForNode(
      'DoStatement_body',
      node.body,
      allowedKeywords: statementKeywords,
    );
    _recordDataForNode(
      'DoStatement_condition',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    super.visitDoStatement(node);
  }

  @override
  void visitDottedName(DottedName node) {
    // The components are always identifiers.
    super.visitDottedName(node);
  }

  @override
  void visitDoubleLiteral(DoubleLiteral node) {
    // There are no completions.
    super.visitDoubleLiteral(node);
  }

  @override
  void visitEmptyFunctionBody(EmptyFunctionBody node) {
    // There are no completions.
    super.visitEmptyFunctionBody(node);
  }

  @override
  void visitEmptyStatement(EmptyStatement node) {
    // There are no completions.
    super.visitEmptyStatement(node);
  }

  @override
  void visitEnumConstantDeclaration(EnumConstantDeclaration node) {
    // There are no completions.
    super.visitEnumConstantDeclaration(node);
  }

  @override
  void visitEnumDeclaration(EnumDeclaration node) {
    // There are no completions.
    super.visitEnumDeclaration(node);
  }

  @override
  void visitExportDirective(ExportDirective node) {
    var context = 'uri';
    if (node.configurations.isNotEmpty) {
      _recordKeyword(
        'ImportDirective_$context',
        node.configurations[0],
        allowedKeywords: exportKeywords,
      );
      context = 'configurations';
    }
    if (node.combinators.isNotEmpty) {
      _recordKeyword(
        'ImportDirective_$context',
        node.combinators[0],
        allowedKeywords: exportKeywords,
      );
    }
    for (var combinator in node.combinators) {
      _recordKeyword(
        'ImportDirective_combinator',
        combinator,
        allowedKeywords: exportKeywords,
      );
    }
    super.visitExportDirective(node);
  }

  @override
  void visitExpressionFunctionBody(ExpressionFunctionBody node) {
    _recordKeyword(
      'ExpressionFunctionBody_start',
      node,
      allowedKeywords: functionBodyKeywords,
    );
    _recordDataForNode(
      'ExpressionFunctionBody_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitExpressionFunctionBody(node);
  }

  @override
  void visitExpressionStatement(ExpressionStatement node) {
    _recordDataForNode(
      'ExpressionStatement_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitExpressionStatement(node);
  }

  @override
  void visitExtendsClause(ExtendsClause node) {
    _recordDataForNode('ExtendsClause_superclass', node.superclass);
    super.visitExtendsClause(node);
  }

  @override
  void visitExtensionDeclaration(ExtensionDeclaration node) {
    _recordDataForNode('ExtensionDeclaration_onClause', node.onClause);
    for (var member in node.body.members) {
      _recordDataForNode(
        'ExtensionDeclaration_member',
        member,
        allowedKeywords: memberKeywords,
      );
    }
    super.visitExtensionDeclaration(node);
  }

  @override
  void visitExtensionOverride(ExtensionOverride node) {
    // There are no completions.
    super.visitExtensionOverride(node);
  }

  @override
  void visitFieldDeclaration(FieldDeclaration node) {
    _recordDataForNode('FieldDeclaration_fields', node.fields);
    super.visitFieldDeclaration(node);
  }

  @override
  void visitFieldFormalParameter(FieldFormalParameter node) {
    // The completions after `this.` are always existing fields.
    super.visitFieldFormalParameter(node);
  }

  @override
  void visitForEachPartsWithDeclaration(ForEachPartsWithDeclaration node) {
    _recordDataForNode(
      'ForEachPartsWithDeclaration_loopVariable',
      node.loopVariable,
    );
    _recordDataForNode(
      'ForEachPartsWithDeclaration_iterable',
      node.iterable,
      allowedKeywords: expressionKeywords,
    );
    super.visitForEachPartsWithDeclaration(node);
  }

  @override
  void visitForEachPartsWithIdentifier(ForEachPartsWithIdentifier node) {
    _recordDataForNode(
      'ForEachPartsWithIdentifier_identifier',
      node.identifier,
    );
    _recordDataForNode(
      'ForEachPartsWithIdentifier_iterable',
      node.iterable,
      allowedKeywords: expressionKeywords,
    );
    super.visitForEachPartsWithIdentifier(node);
  }

  @override
  void visitForElement(ForElement node) {
    _recordDataForNode('ForElement_forLoopParts', node.forLoopParts);
    _recordDataForNode('ForElement_body', node.body);
    super.visitForElement(node);
  }

  @override
  void visitFormalParameterList(FormalParameterList node) {
    for (var parameter in node.parameters) {
      _recordDataForNode(
        'FormalParameterList_parameter',
        parameter,
        allowedKeywords: [Keyword.COVARIANT],
      );
    }
    super.visitFormalParameterList(node);
  }

  @override
  void visitForPartsWithDeclarations(ForPartsWithDeclarations node) {
    _recordDataForNode(
      'ForParts_condition',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    for (var updater in node.updaters) {
      _recordDataForNode(
        'ForParts_updater',
        updater,
        allowedKeywords: expressionKeywords,
      );
    }
    super.visitForPartsWithDeclarations(node);
  }

  @override
  void visitForPartsWithExpression(ForPartsWithExpression node) {
    _recordDataForNode(
      'ForParts_condition',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    for (var updater in node.updaters) {
      _recordDataForNode(
        'ForParts_updater',
        updater,
        allowedKeywords: expressionKeywords,
      );
    }
    super.visitForPartsWithExpression(node);
  }

  @override
  void visitForStatement(ForStatement node) {
    _recordDataForNode('ForStatement_forLoopParts', node.forLoopParts);
    _recordDataForNode(
      'ForStatement_body',
      node.body,
      allowedKeywords: statementKeywords,
    );
    super.visitForStatement(node);
  }

  @override
  void visitFunctionDeclaration(FunctionDeclaration node) {
    _recordDataForNode('FunctionDeclaration_returnType', node.returnType);
    super.visitFunctionDeclaration(node);
  }

  @override
  void visitFunctionDeclarationStatement(FunctionDeclarationStatement node) {
    // There are no completions.
    super.visitFunctionDeclarationStatement(node);
  }

  @override
  void visitFunctionExpression(FunctionExpression node) {
    // There are no completions.
    super.visitFunctionExpression(node);
  }

  @override
  void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
    // There are no completions.
    super.visitFunctionExpressionInvocation(node);
  }

  @override
  void visitFunctionTypeAlias(FunctionTypeAlias node) {
    // There are no completions.
    super.visitFunctionTypeAlias(node);
  }

  @override
  void visitFunctionTypedFormalParameter(FunctionTypedFormalParameter node) {
    // There are no completions.
    super.visitFunctionTypedFormalParameter(node);
  }

  @override
  void visitGenericFunctionType(GenericFunctionType node) {
    // There are no completions.
    super.visitGenericFunctionType(node);
  }

  @override
  void visitGenericTypeAlias(GenericTypeAlias node) {
    _recordDataForNode(
      'GenericTypeAlias_type',
      node.functionType,
      allowedKeywords: [Keyword.FUNCTION],
    );
    super.visitGenericTypeAlias(node);
  }

  @override
  void visitHideCombinator(HideCombinator node) {
    for (var hiddenName in node.hiddenNames) {
      _recordDataForNode('HideCombinator_hiddenName', hiddenName);
    }
    super.visitHideCombinator(node);
  }

  @override
  void visitIfElement(IfElement node) {
    _recordDataForNode(
      'IfElement_condition',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode('IfElement_thenElement', node.thenElement);
    _recordDataForNode('IfElement_elseElement', node.elseElement);
    super.visitIfElement(node);
  }

  @override
  void visitIfStatement(IfStatement node) {
    _recordDataForNode(
      'IfStatement_condition',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode(
      'IfStatement_thenStatement',
      node.thenStatement,
      allowedKeywords: statementKeywords,
    );
    _recordDataForNode(
      'IfStatement_elseStatement',
      node.elseStatement,
      allowedKeywords: statementKeywords,
    );
    super.visitIfStatement(node);
  }

  @override
  void visitImplementsClause(ImplementsClause node) {
    // At the start of each type name.
    for (var namedType in node.interfaces) {
      _recordDataForNode('ImplementsClause_interface', namedType);
    }
    super.visitImplementsClause(node);
  }

  @override
  void visitImportDirective(ImportDirective node) {
    var context = 'uri';
    var deferredKeyword = node.deferredKeyword;
    if (deferredKeyword != null) {
      data.recordKeyword('ImportDirective_$context', deferredKeyword.keyword!);
      context = 'deferred';
    }
    var asKeyword = node.asKeyword;
    if (asKeyword != null) {
      data.recordKeyword('ImportDirective_$context', asKeyword.keyword!);
      context = 'prefix';
    }
    if (node.configurations.isNotEmpty) {
      _recordKeyword(
        'ImportDirective_$context',
        node.configurations[0],
        allowedKeywords: importKeywords,
      );
      context = 'configurations';
    }
    if (node.combinators.isNotEmpty) {
      _recordKeyword(
        'ImportDirective_$context',
        node.combinators[0],
        allowedKeywords: importKeywords,
      );
    }
    for (var combinator in node.combinators) {
      _recordKeyword(
        'ImportDirective_combinator',
        combinator,
        allowedKeywords: importKeywords,
      );
    }
    super.visitImportDirective(node);
  }

  @override
  void visitIndexExpression(IndexExpression node) {
    _recordDataForNode(
      'IndexExpression_index',
      node.index,
      allowedKeywords: expressionKeywords,
    );
    super.visitIndexExpression(node);
  }

  @override
  void visitInstanceCreationExpression(InstanceCreationExpression node) {
    _recordDataForNode(
      'InstanceCreationExpression_constructorName',
      node.constructorName,
    );
    super.visitInstanceCreationExpression(node);
  }

  @override
  void visitIntegerLiteral(IntegerLiteral node) {
    // There are no completions.
    super.visitIntegerLiteral(node);
  }

  @override
  void visitInterpolationExpression(InterpolationExpression node) {
    // TODO(brianwilkerson): Consider splitting this based on whether the
    //  expression is a simple identifier ('$') or a full expression ('${').
    _recordDataForNode(
      'InterpolationExpression_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitInterpolationExpression(node);
  }

  @override
  void visitInterpolationString(InterpolationString node) {
    // There are no completions.
    super.visitInterpolationString(node);
  }

  @override
  void visitIsExpression(IsExpression node) {
    _recordDataForNode('IsExpression_type', node.type);
    super.visitIsExpression(node);
  }

  @override
  void visitLabel(Label node) {
    // There are no completions.
    super.visitLabel(node);
  }

  @override
  void visitLabeledStatement(LabeledStatement node) {
    _recordDataForNode(
      'LabeledStatement_statement',
      node.statement,
      allowedKeywords: statementKeywords,
    );
    super.visitLabeledStatement(node);
  }

  @override
  void visitLibraryDirective(LibraryDirective node) {
    // There are no completions.
    super.visitLibraryDirective(node);
  }

  @override
  void visitLibraryIdentifier(LibraryIdentifier node) {
    // There are no completions.
    super.visitLibraryIdentifier(node);
  }

  @override
  void visitListLiteral(ListLiteral node) {
    for (var element in node.elements) {
      _recordDataForNode(
        'ListLiteral_element',
        element,
        allowedKeywords: expressionKeywords,
      );
    }
    super.visitListLiteral(node);
  }

  @override
  void visitMapLiteralEntry(MapLiteralEntry node) {
    _recordDataForNode(
      'MapLiteralEntry_value',
      node.value,
      allowedKeywords: expressionKeywords,
    );
    super.visitMapLiteralEntry(node);
  }

  @override
  void visitMethodDeclaration(MethodDeclaration node) {
    _recordDataForNode('MethodDeclaration_returnType', node.returnType);
    super.visitMethodDeclaration(node);
  }

  @override
  void visitMethodInvocation(MethodInvocation node) {
    // There are no completions.
    super.visitMethodInvocation(node);
  }

  @override
  void visitMixinDeclaration(MixinDeclaration node) {
    var context = 'name';
    if (node.onClause != null) {
      _recordKeyword(
        'MixinDeclaration_$context',
        node.onClause,
        allowedKeywords: [Keyword.ON],
      );
      context = 'on';
    }
    _recordKeyword(
      'MixinDeclaration_$context',
      node.implementsClause,
      allowedKeywords: [Keyword.IMPLEMENTS],
    );

    for (var member in node.body.members) {
      _recordDataForNode(
        'MixinDeclaration_member',
        member,
        allowedKeywords: memberKeywords,
      );
    }
    super.visitMixinDeclaration(node);
  }

  @override
  void visitMixinOnClause(MixinOnClause node) {
    for (var constraint in node.superclassConstraints) {
      _recordDataForNode('OnClause_superclassConstraint', constraint);
    }
    super.visitMixinOnClause(node);
  }

  @override
  void visitNamedExpression(NamedExpression node) {
    // Named expressions only occur in argument lists and are handled there.
    super.visitNamedExpression(node);
  }

  @override
  void visitNamedType(NamedType node) {
    // There are no completions.
    super.visitNamedType(node);
  }

  @override
  void visitNativeClause(NativeClause node) {
    // There are no completions.
    super.visitNativeClause(node);
  }

  @override
  void visitNativeFunctionBody(NativeFunctionBody node) {
    // There are no completions.
    super.visitNativeFunctionBody(node);
  }

  @override
  void visitNullLiteral(NullLiteral node) {
    // There are no completions.
    super.visitNullLiteral(node);
  }

  @override
  void visitParenthesizedExpression(ParenthesizedExpression node) {
    _recordDataForNode(
      'ParenthesizedExpression_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitParenthesizedExpression(node);
  }

  @override
  void visitPartDirective(PartDirective node) {
    // There are no completions.
    super.visitPartDirective(node);
  }

  @override
  void visitPartOfDirective(PartOfDirective node) {
    // There are no completions.
    super.visitPartOfDirective(node);
  }

  @override
  void visitPostfixExpression(PostfixExpression node) {
    // There are no completions.
    super.visitPostfixExpression(node);
  }

  @override
  void visitPrefixedIdentifier(PrefixedIdentifier node) {
    // There are no completions.
    super.visitPrefixedIdentifier(node);
  }

  @override
  void visitPrefixExpression(PrefixExpression node) {
    _recordDataForNode(
      'PrefixExpression_${node.operator}_operand',
      node.operand,
      allowedKeywords: expressionKeywords,
    );
    super.visitPrefixExpression(node);
  }

  @override
  void visitPropertyAccess(PropertyAccess node) {
    _recordDataForNode('PropertyAccess_propertyName', node.propertyName);
    super.visitPropertyAccess(node);
  }

  @override
  void visitRedirectingConstructorInvocation(
    RedirectingConstructorInvocation node,
  ) {
    // There are no completions.
    super.visitRedirectingConstructorInvocation(node);
  }

  @override
  void visitRethrowExpression(RethrowExpression node) {
    // There are no completions.
    super.visitRethrowExpression(node);
  }

  @override
  void visitReturnStatement(ReturnStatement node) {
    _recordDataForNode(
      'ReturnStatement_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitReturnStatement(node);
  }

  @override
  void visitScriptTag(ScriptTag node) {
    // There are no completions.
    super.visitScriptTag(node);
  }

  @override
  void visitSetOrMapLiteral(SetOrMapLiteral node) {
    for (var element in node.elements) {
      _recordDataForNode(
        'SetOrMapLiteral_element',
        element,
        allowedKeywords: expressionKeywords,
      );
    }
    super.visitSetOrMapLiteral(node);
  }

  @override
  void visitShowCombinator(ShowCombinator node) {
    for (var name in node.shownNames) {
      _recordDataForNode('ShowCombinator_shownName', name);
    }
    super.visitShowCombinator(node);
  }

  @override
  void visitSimpleFormalParameter(SimpleFormalParameter node) {
    // There are no completions.
    super.visitSimpleFormalParameter(node);
  }

  @override
  void visitSimpleStringLiteral(SimpleStringLiteral node) {
    // There are no completions.
    super.visitSimpleStringLiteral(node);
  }

  @override
  void visitSpreadElement(SpreadElement node) {
    _recordDataForNode(
      'SpreadElement_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitSpreadElement(node);
  }

  @override
  void visitStringInterpolation(StringInterpolation node) {
    // There are no completions.
    super.visitStringInterpolation(node);
  }

  @override
  void visitSuperConstructorInvocation(SuperConstructorInvocation node) {
    // There are no completions.
    super.visitSuperConstructorInvocation(node);
  }

  @override
  void visitSuperExpression(SuperExpression node) {
    // There are no completions.
    super.visitSuperExpression(node);
  }

  @override
  void visitSwitchCase(SwitchCase node) {
    _recordDataForNode(
      'SwitchCase_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    for (var statement in node.statements) {
      _recordDataForNode(
        'SwitchMember_statement',
        statement,
        allowedKeywords: statementKeywords,
      );
    }
    super.visitSwitchCase(node);
  }

  @override
  void visitSwitchDefault(SwitchDefault node) {
    for (var statement in node.statements) {
      _recordDataForNode(
        'SwitchMember_statement',
        statement,
        allowedKeywords: statementKeywords,
      );
    }
    super.visitSwitchDefault(node);
  }

  @override
  void visitSwitchStatement(SwitchStatement node) {
    _recordDataForNode(
      'SwitchStatement_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitSwitchStatement(node);
  }

  @override
  void visitSymbolLiteral(SymbolLiteral node) {
    // There are no completions.
    super.visitSymbolLiteral(node);
  }

  @override
  void visitThisExpression(ThisExpression node) {
    // There are no completions.
    super.visitThisExpression(node);
  }

  @override
  void visitThrowExpression(ThrowExpression node) {
    _recordDataForNode(
      'ThrowExpression_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitThrowExpression(node);
  }

  @override
  void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) {
    // There are no completions.
    super.visitTopLevelVariableDeclaration(node);
  }

  @override
  void visitTryStatement(TryStatement node) {
    var context = 'try';
    for (var clause in node.catchClauses) {
      _recordKeyword(
        'TryStatement_$context',
        clause,
        allowedKeywords: [Keyword.ON],
      );
      context = 'catch';
    }
    var finallyKeyword = node.finallyKeyword;
    if (finallyKeyword != null) {
      data.recordKeyword('TryStatement_$context', finallyKeyword.keyword!);
    }
    super.visitTryStatement(node);
  }

  @override
  void visitTypeArgumentList(TypeArgumentList node) {
    for (var typeArgument in node.arguments) {
      _recordDataForNode('TypeArgumentList_argument', typeArgument);
    }
    super.visitTypeArgumentList(node);
  }

  @override
  void visitTypeParameter(TypeParameter node) {
    if (node.bound != null) {
      _recordDataForNode('TypeParameter_bound', node.bound);
    }
    super.visitTypeParameter(node);
  }

  @override
  void visitTypeParameterList(TypeParameterList node) {
    // There are no completions.
    super.visitTypeParameterList(node);
  }

  @override
  void visitVariableDeclaration(VariableDeclaration node) {
    var keywords = node.parent?.parent is FieldDeclaration
        ? [Keyword.COVARIANT, ...expressionKeywords]
        : expressionKeywords;
    _recordDataForNode(
      'VariableDeclaration_initializer',
      node.initializer,
      allowedKeywords: keywords,
    );
    super.visitVariableDeclaration(node);
  }

  @override
  void visitVariableDeclarationList(VariableDeclarationList node) {
    _recordDataForNode('VariableDeclarationList_type', node.type);
    super.visitVariableDeclarationList(node);
  }

  @override
  void visitVariableDeclarationStatement(VariableDeclarationStatement node) {
    // There are no completions.
    super.visitVariableDeclarationStatement(node);
  }

  @override
  void visitWhileStatement(WhileStatement node) {
    _recordDataForNode(
      'WhileStatement_condition',
      node.condition,
      allowedKeywords: expressionKeywords,
    );
    _recordDataForNode(
      'WhileStatement_body',
      node.body,
      allowedKeywords: statementKeywords,
    );
    super.visitWhileStatement(node);
  }

  @override
  void visitWithClause(WithClause node) {
    for (var namedType in node.mixinTypes) {
      _recordDataForNode('WithClause_mixinType', namedType);
    }
    super.visitWithClause(node);
  }

  @override
  void visitYieldStatement(YieldStatement node) {
    _recordDataForNode(
      'YieldStatement_expression',
      node.expression,
      allowedKeywords: expressionKeywords,
    );
    super.visitYieldStatement(node);
  }

  /// Return the context in which the [node] occurs. The [node] is expected to
  /// be the parent of the argument expression.
  String _argumentListContext(AstNode node) {
    if (node is ArgumentList) {
      var parent = node.parent;
      if (parent is Annotation) {
        return 'annotation';
      } else if (parent is ExtensionOverride) {
        return 'extensionOverride';
      } else if (parent is FunctionExpressionInvocation) {
        return 'function';
      } else if (parent is InstanceCreationExpression) {
        if (parent.staticType.isWidgetType) {
          return 'widgetConstructor';
        }
        return 'constructor';
      } else if (parent is MethodInvocation) {
        return 'method';
      } else if (parent is RedirectingConstructorInvocation) {
        return 'constructorRedirect';
      } else if (parent is SuperConstructorInvocation) {
        return 'constructorRedirect';
      }
    } else if (node is AssignmentExpression ||
        node is BinaryExpression ||
        node is PrefixExpression ||
        node is PostfixExpression) {
      return 'operator';
    } else if (node is IndexExpression) {
      return 'index';
    }
    throw ArgumentError(
      'Unknown parent of ${node.runtimeType}: ${node.parent.runtimeType}',
    );
  }

  /// Return the first child of the [node] that is neither a comment nor an
  /// annotation.
  SyntacticEntity? _firstChild(AstNode node) {
    var children = node.childEntities.toList();
    for (var i = 0; i < children.length; i++) {
      var child = children[i];
      if (child is! Comment && child is! Annotation) {
        return child;
      }
    }
    return null;
  }

  /// Return the element associated with the left-most identifier that is a
  /// child of the [node].
  Element? _leftMostElement(AstNode node) => _leftMostIdentifier(node)?.element;

  /// Return the left-most child of the [node] if it is a simple identifier, or
  /// `null` if the left-most child is not a simple identifier. Comments and
  /// annotations are ignored for this purpose.
  SimpleIdentifier? _leftMostIdentifier(AstNode node) {
    AstNode? currentNode = node;
    while (currentNode != null && currentNode is! SimpleIdentifier) {
      var firstChild = _firstChild(currentNode);
      if (firstChild is AstNode) {
        currentNode = firstChild;
      } else {
        currentNode = null;
      }
    }
    if (currentNode is SimpleIdentifier &&
        !currentNode.inDeclarationContext()) {
      return currentNode;
    }
    return null;
  }

  /// Return the element kind of the element associated with the left-most
  /// identifier that is a child of the [node].
  ElementKind? _leftMostKind(AstNode node) {
    if (node is InstanceCreationExpression) {
      return featureComputer.computeElementKind2(node.constructorName.element!);
    }
    var element = _leftMostElement(node);
    if (element == null) {
      return null;
    }
    if (element is InterfaceElement) {
      var parent = node.parent;
      if (parent is Annotation && parent.arguments != null) {
        element = parent.element!;
      }
    }
    return featureComputer.computeElementKind2(element);
  }

  /// Return the left-most token that is a child of the [node].
  Token? _leftMostToken(AstNode node) {
    SyntacticEntity? entity = node;
    while (entity is AstNode) {
      entity = _firstChild(entity);
    }
    if (entity is Token) {
      return entity;
    }
    return null;
  }

  /// Record information about the given [node] occurring in the given
  /// [context].
  void _recordDataForNode(
    String context,
    AstNode? node, {
    List<Keyword> allowedKeywords = noKeywords,
  }) {
    _recordElementKind(context, node);
    _recordKeyword(context, node, allowedKeywords: allowedKeywords);
  }

  /// Record the element kind of the element associated with the left-most
  /// identifier that is a child of the [node] in the given [context].
  void _recordElementKind(String context, AstNode? node) {
    if (node != null) {
      var kind = _leftMostKind(node);
      if (kind != null) {
        data.recordElementKind(context, kind);
        if (node is Expression) {
          data.recordElementKind('Expression', kind);
        } else if (node is Statement) {
          data.recordElementKind('Statement', kind);
        }
      }
    }
  }

  /// If the left-most token of the [node] is a keyword, then record that it
  /// occurred in the given [context].
  void _recordKeyword(
    String context,
    AstNode? node, {
    List<Keyword> allowedKeywords = noKeywords,
  }) {
    if (node != null) {
      var token = _leftMostToken(node);
      if (token != null && token.isKeyword) {
        var keyword = token.keyword!;
        if (keyword == Keyword.NEW) {
          // We don't suggest `new`, so we don't care about the frequency with
          // which it is used.
          return;
        } else if (keyword.isBuiltInOrPseudo &&
            !allowedKeywords.contains(keyword)) {
          // These keywords can be used as identifiers, so determine whether
          // it is being used as a keyword or an identifier.
          return;
        }
        data.recordKeyword(context, keyword);
      }
    }
  }
}

/// An object used to compute metrics for a single file or directory.
class RelevanceMetricsComputer {
  /// The metrics data that was computed.
  final RelevanceData data = RelevanceData();

  /// Initialize a newly created metrics computer that can compute the metrics
  /// in one or more files and directories.
  RelevanceMetricsComputer();

  /// Compute the metrics for the file(s) in the [rootPath].
  /// If [corpus] is true, treat rootPath as a container of packages, creating
  /// a new context collection for each subdirectory.
  Future<void> compute(String rootPath, {required bool verbose}) async {
    var collection = AnalysisContextCollection(
      includedPaths: [rootPath],
      resourceProvider: PhysicalResourceProvider.INSTANCE,
    );
    var collector = RelevanceDataCollector(data);
    for (var context in collection.contexts) {
      await _computeInContext(context.contextRoot, collector, verbose: verbose);
    }
  }

  /// Compute the metrics for the files in the context [root], creating a
  /// separate context collection to prevent accumulating memory. The metrics
  /// should be captured in the [collector]. Include additional details in the
  /// output if [verbose] is `true`.
  Future<void> _computeInContext(
    ContextRoot root,
    RelevanceDataCollector collector, {
    required bool verbose,
  }) async {
    // Create a new collection to avoid consuming large quantities of memory.
    var collection = AnalysisContextCollection(
      includedPaths: root.includedPaths.toList(),
      excludedPaths: root.excludedPaths.toList(),
      resourceProvider: PhysicalResourceProvider.INSTANCE,
    );
    var context = collection.contexts[0];
    var pathContext = context.contextRoot.resourceProvider.pathContext;
    for (var filePath in context.contextRoot.analyzedFiles()) {
      if (file_paths.isDart(pathContext, filePath)) {
        try {
          var resolvedUnitResult = await context.currentSession.getResolvedUnit(
            filePath,
          );
          //
          // Check for errors that cause the file to be skipped.
          //
          if (resolvedUnitResult is! ResolvedUnitResult) {
            print('File $filePath skipped because it could not be analyzed.');
            if (verbose) {
              print('');
            }
            continue;
          } else if (resolvedUnitResult.diagnostics.errors.isNotEmpty) {
            if (verbose) {
              print('File $filePath skipped due to errors:');
              for (var diagnostic in resolvedUnitResult.diagnostics.errors) {
                print('  ${diagnostic.toString()}');
              }
              print('');
            } else {
              print('File $filePath skipped due to analysis errors.');
            }
            continue;
          }

          collector.initializeFrom(resolvedUnitResult);
          resolvedUnitResult.unit.accept(collector);
        } catch (exception, stacktrace) {
          print('Exception caught analyzing: "$filePath"');
          print(exception);
          print(stacktrace);
        }
      }
    }
  }
}

/// A class used to write relevance data as a set of tables in a generated Dart
/// file.
class RelevanceTableWriter {
  final StringSink sink;

  RelevanceTableWriter(this.sink);

  void write(RelevanceData data) {
    writeFileHeader();
    writeElementKindTable(data);
    writeKeywordTable(data);
    writeFileFooter();
  }

  void writeElementKindTable(RelevanceData data) {
    sink.writeln();
    sink.write('''
const defaultElementKindRelevance = {
''');

    var byKind = data._byKind;
    var entries = byKind.entries.toList()
      ..sort((first, second) => first.key.compareTo(second.key));
    for (var entry in entries) {
      var completionLocation = entry.key;
      var counts = entry.value;
      if (_hasElementKind(counts)) {
        var totalCount = _totalCount(counts);
        // TODO(brianwilkerson): If two element kinds have the same count they
        //  ought to have the same probability. This doesn't correctly do that.
        var entries = counts.entries.toList()
          ..sort((first, second) => first.value.compareTo(second.value));

        sink.write("  '");
        sink.write(completionLocation);
        sink.writeln("': {");
        var cumulativeCount = 0;
        var lowerBound = 0.0;
        for (var entry in entries) {
          var kind = entry.key;
          cumulativeCount += entry.value;
          var upperBound = cumulativeCount / totalCount;
          if (kind is _ElementKind) {
            sink.write('    ElementKind.');
            sink.write(kind.elementKind.name);
            sink.write(': ProbabilityRange(lower: ');
            sink.write(lowerBound.toStringAsFixed(3));
            sink.write(', upper: ');
            sink.write(upperBound.toStringAsFixed(3));
            sink.writeln('),');
          }
          lowerBound = upperBound;
        }
        sink.writeln('  },');
      }
    }
    sink.writeln('};');
  }

  void writeFileFooter() {
    sink.write('''
/// A table keyed by completion location and element kind whose values are the
/// ranges of the relevance of those element kinds in those locations.
Map<String, Map<ElementKind, ProbabilityRange>> elementKindRelevance =
    defaultElementKindRelevance;

/// A table keyed by completion location and keyword whose values are the
/// ranges of the relevance of those keywords in those locations.
Map<String, Map<String, ProbabilityRange>> keywordRelevance =
    defaultKeywordRelevance;
''');
  }

  void writeFileHeader() {
    sink.write('''
// Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// This file has been automatically generated. Please do not edit it manually.
// To regenerate the file, use the script
// "pkg/analysis_server/tool/completion_metrics/relevance_table_generator.dart",
// passing it the location of a corpus of code that is large enough for the
// computed values to be statistically meaningful.

import 'package:analysis_server/src/services/completion/dart/probability_range.dart';
import 'package:analyzer_plugin/protocol/protocol_common.dart';
''');
  }

  void writeKeywordTable(RelevanceData data) {
    sink.writeln();
    sink.write('''
const defaultKeywordRelevance = {
''');

    var byKind = data._byKind;
    var entries = byKind.entries.toList()
      ..sort((first, second) => first.key.compareTo(second.key));
    for (var entry in entries) {
      var completionLocation = entry.key;
      var counts = entry.value;
      if (_hasKeyword(counts)) {
        var totalCount = _totalCount(counts);
        // TODO(brianwilkerson): If two keywords have the same count they ought to
        //  have the same probability. This doesn't correctly do that.
        var entries = counts.entries.toList()
          ..sort((first, second) => first.value.compareTo(second.value));

        sink.write("  '");
        sink.write(completionLocation);
        sink.writeln("': {");
        var cumulativeCount = 0;
        var lowerBound = 0.0;
        for (var entry in entries) {
          var kind = entry.key;
          cumulativeCount += entry.value;
          var upperBound = cumulativeCount / totalCount;
          if (kind is _Keyword) {
            sink.write("    '");
            sink.write(kind.keyword.lexeme);
            sink.write("': ProbabilityRange(lower: ");
            sink.write(lowerBound.toStringAsFixed(3));
            sink.write(', upper: ');
            sink.write(upperBound.toStringAsFixed(3));
            sink.writeln('),');
          }
          lowerBound = upperBound;
        }
        sink.writeln('  },');
      }
    }
    sink.writeln('};');
  }

  /// Return `true` if the table of [counts] contains at least one key that is
  /// an element kind.
  bool _hasElementKind(Map<_Kind, int> counts) {
    for (var kind in counts.keys) {
      if (kind is _ElementKind) {
        return true;
      }
    }
    return false;
  }

  /// Return `true` if the table of [counts] contains at least one key that is a
  /// keyword.
  bool _hasKeyword(Map<_Kind, int> counts) {
    for (var kind in counts.keys) {
      if (kind is _Keyword) {
        return true;
      }
    }
    return false;
  }

  /// Return the total of the counts in the given table of [counts].
  int _totalCount(Map<_Kind, int> counts) {
    return counts.values.fold(
      0,
      (previousValue, value) => previousValue + value,
    );
  }
}

/// A wrapper for an element kind to allow keywords and element kinds to be used
/// as keys in a single table.
class _ElementKind extends _Kind {
  static final Map<ElementKind, _ElementKind> instances = {};

  final ElementKind elementKind;

  factory _ElementKind(ElementKind elementKind) =>
      instances.putIfAbsent(elementKind, () => _ElementKind._(elementKind));

  _ElementKind._(this.elementKind);

  @override
  String get uniqueKey => 'e${elementKind.name}';
}

/// A wrapper for a keyword to allow keywords and element kinds to be used as
/// keys in a single table.
class _Keyword extends _Kind {
  static final Map<Keyword, _Keyword> instances = {};

  final Keyword keyword;

  factory _Keyword(Keyword keyword) =>
      instances.putIfAbsent(keyword, () => _Keyword._(keyword));

  _Keyword._(this.keyword);

  @override
  String get uniqueKey => 'k${keyword.lexeme}';
}

/// A superclass for [_ElementKind] and [_Keyword] to allow keywords and element
/// kinds to be used as keys in a single table.
abstract class _Kind {
  /// Return the unique key used when representing an instance of a subclass in
  /// a JSON format.
  String get uniqueKey;
}
